<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Step Sequencer: MDModemnz</title>
    <meta
      name="description"
      content="Making an instrument with the Web Audio API" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <div class="loading">
      <p>Loading...</p>
    </div>

    <div id="sequencer">
      <section class="controls-main">
        <h1>ModemDN</h1>
        <label for="bpm">BPM</label>
        <input
          name="bpm"
          id="bpm"
          type="range"
          min="60"
          max="180"
          value="120"
          step="1" />
        <span id="bpmval">120</span>
        <input type="checkbox" id="playBtn" />
        <label for="playBtn">Play</label>
      </section>

      <div id="tracks">
        <!-- track one: bleep -->
        <section class="track-one">
          <h2>Sweep</h2>
          <section class="controls">
            <label for="attack">Att</label>
            <input
              name="attack"
              id="attack"
              type="range"
              min="0"
              max="1"
              value="0.2"
              step="0.1" />
            <label for="release">Rel</label>
            <input
              name="release"
              id="release"
              type="range"
              min="0"
              max="1"
              value="0.5"
              step="0.1" />
          </section>

          <section class="pads">
            <input type="checkbox" id="v1n1" />
            <label for="v1n1">Voice 1, Note 1</label>

            <input type="checkbox" id="v1n2" />
            <label for="v1n2">Voice 1, Note 2</label>

            <input type="checkbox" id="v1n3" />
            <label for="v1n3">Voice 1, Note 3</label>

            <input type="checkbox" id="v1n4" />
            <label for="v1n4">Voice 1, Note 4</label>
          </section>
        </section>

        <!-- track two: pad/sweep -->
        <section class="track-two">
          <h2>Pulse</h2>
          <section class="controls">
            <label for="hz">Hz</label>
            <input
              name="hz"
              id="hz"
              type="range"
              min="660"
              max="1320"
              value="880"
              step="1" />
            <label for="lfo">LFO</label>
            <input
              name="lfo"
              id="lfo"
              type="range"
              min="20"
              max="40"
              value="30"
              step="1" />
          </section>
          <!--

          -->
          <section class="pads">
            <input type="checkbox" id="v2n1" />
            <label for="v2n1">Voice 2, Note 1</label>

            <input type="checkbox" id="v2n2" />
            <label for="v2n2">Voice 2, Note 2</label>

            <input type="checkbox" id="v2n3" />
            <label for="v2n3">Voice 2, Note 3</label>

            <input type="checkbox" id="v2n4" />
            <label for="v2n4">Voice 2, Note 4</label>
          </section>
        </section>

        <!-- track three: noise -->
        <section class="track-three">
          <h2>Noise</h2>
          <section class="controls">
            <label for="duration">Dur</label>
            <input
              name="duration"
              id="duration"
              type="range"
              min="0.25"
              max="1"
              value="1"
              step="0.25" />
            <label for="band">Band</label>
            <input
              name="band"
              id="band"
              type="range"
              min="400"
              max="1200"
              value="1000"
              step="5" />
          </section>

          <section class="pads">
            <input type="checkbox" id="v3n1" />
            <label for="v3n1">Voice 3, Note 1</label>

            <input type="checkbox" id="v3n2" />
            <label for="v3n2">Voice 3, Note 2</label>

            <input type="checkbox" id="v3n3" />
            <label for="v3n3">Voice 3, Note 3</label>

            <input type="checkbox" id="v3n4" />
            <label for="v3n4">Voice 3, Note 4</label>
          </section>
        </section>

        <!-- track four: drill -->
        <section class="track-four">
          <h2>DTMF</h2>
          <section class="controls">
            <label for="rate">Rate</label>
            <input
              name="rate"
              id="rate"
              type="range"
              min="0.1"
              max="2"
              value="1"
              step="0.1" />
          </section>
          <!--

          -->
          <section class="pads">
            <input type="checkbox" id="v4n1" />
            <label for="v4n1">Voice 4, Note 1</label>

            <input type="checkbox" id="v4n2" />
            <label for="v4n2">Voice 4, Note 2</label>

            <input type="checkbox" id="v4n3" />
            <label for="v4n3">Voice 4, Note 3</label>

            <input type="checkbox" id="v4n4" />
            <label for="v4n4">Voice 4, Note 4</label>
          </section>
        </section>
      </div>
    </div>
    <!-- sequencer -->

    <script src="wavetable.js"></script>

    <script>
      const audioCtx = new AudioContext();

      // Before we do anything more, let's grab our checkboxes from the interface. We want to keep them in the groups they are in as each row represents a different sound or _voice_.
      const pads = document.querySelectorAll(".pads");

      const wave = new PeriodicWave(audioCtx, {
        real: wavetable.real,
        imag: wavetable.imag,
      });

      let attackTime = 0.2;
      const attackControl = document.querySelector("#attack");
      attackControl.addEventListener(
        "input",
        (ev) => {
          attackTime = parseFloat(ev.target.value);
        },
        false
      );

      let releaseTime = 0.5;
      const releaseControl = document.querySelector("#release");
      releaseControl.addEventListener(
        "input",
        (ev) => {
          releaseTime = parseFloat(ev.target.value);
        },
        false
      );

      // Expose attack time & release time
      const sweepLength = 2;
      function playSweep(time) {
        const osc = new OscillatorNode(audioCtx, {
          frequency: 380,
          type: "custom",
          periodicWave: wave,
        });

        const sweepEnv = new GainNode(audioCtx);
        sweepEnv.gain.cancelScheduledValues(time);
        sweepEnv.gain.setValueAtTime(0, time);
        sweepEnv.gain.linearRampToValueAtTime(1, time + attackTime);
        sweepEnv.gain.linearRampToValueAtTime(
          0,
          time + sweepLength - releaseTime
        );

        osc.connect(sweepEnv).connect(audioCtx.destination);
        osc.start(time);
        osc.stop(time + sweepLength);
      }

      // Expose frequency & frequency modulation
      let pulseHz = 880;
      const hzControl = document.querySelector("#hz");
      hzControl.addEventListener(
        "input",
        (ev) => {
          pulseHz = parseFloat(ev.target.value);
        },
        false
      );

      let lfoHz = 30;
      const lfoControl = document.querySelector("#lfo");
      lfoControl.addEventListener(
        "input",
        (ev) => {
          lfoHz = parseFloat(ev.target.value);
        },
        false
      );

      const pulseTime = 1;
      function playPulse(time) {
        const osc = new OscillatorNode(audioCtx, {
          type: "sine",
          frequency: pulseHz,
        });

        const amp = new GainNode(audioCtx, {
          value: 1,
        });

        const lfo = new OscillatorNode(audioCtx, {
          type: "square",
          frequency: lfoHz,
        });

        lfo.connect(amp.gain);
        osc.connect(amp).connect(audioCtx.destination);
        lfo.start();
        osc.start(time);
        osc.stop(time + pulseTime);
      }

      // Expose noteDuration & band frequency
      let noiseDuration = 1;
      const durControl = document.querySelector("#duration");
      durControl.addEventListener(
        "input",
        (ev) => {
          noiseDuration = parseFloat(ev.target.value);
        },
        false
      );

      let bandHz = 1000;
      const bandControl = document.querySelector("#band");
      bandControl.addEventListener(
        "input",
        (ev) => {
          bandHz = parseFloat(ev.target.value);
        },
        false
      );

      function playNoise(time) {
        const bufferSize = audioCtx.sampleRate * noiseDuration; // set the time of the note

        // Create an empty buffer
        const noiseBuffer = new AudioBuffer({
          length: bufferSize,
          sampleRate: audioCtx.sampleRate,
        });

        // Fill the buffer with noise
        const data = noiseBuffer.getChannelData(0);
        for (let i = 0; i < bufferSize; i++) {
          data[i] = Math.random() * 2 - 1;
        }

        // Create a buffer source for our created data
        const noise = new AudioBufferSourceNode(audioCtx, {
          buffer: noiseBuffer,
        });

        // Filter the output
        const bandpass = new BiquadFilterNode(audioCtx, {
          type: "bandpass",
          frequency: bandHz,
        });

        // Connect our graph
        noise.connect(bandpass).connect(audioCtx.destination);
        noise.start(time);
      }

      // Loading the file: fetch the audio file and decode the data
      async function getFile(audioContext, filepath) {
        const response = await fetch(filepath);
        const arrayBuffer = await response.arrayBuffer();
        const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
        return audioBuffer;
      }

      let playbackRate = 1;
      const rateControl = document.querySelector("#rate");
      rateControl.addEventListener(
        "input",
        (ev) => {
          playbackRate = parseFloat(ev.target.value);
        },
        false
      );

      // Create a buffer, plop in data, connect and play -> modify graph here if required
      function playSample(audioContext, audioBuffer, time) {
        const sampleSource = new AudioBufferSourceNode(audioContext, {
          buffer: audioBuffer,
          playbackRate: playbackRate,
        });
        sampleSource.connect(audioContext.destination);
        sampleSource.start(time);
        return sampleSource;
      }

      async function setupSample() {
        const filePath = "dtmf.mp3";
        // Here we're waiting for the load of the file
        // To be able to use this keyword we need to be within an `async` function
        const sample = await getFile(audioCtx, filePath);
        return sample;
      }

      // Scheduling
      let tempo = 60.0;
      const bpmControl = document.querySelector("#bpm");
      const bpmValEl = document.querySelector("#bpmval");

      bpmControl.addEventListener(
        "input",
        (ev) => {
          tempo = parseFloat(ev.target.value);
          bpmValEl.innerText = tempo;
        },
        false
      );

      const lookahead = 25.0; // How frequently to call scheduling function (in milliseconds)
      const scheduleAheadTime = 0.1; // How far ahead to schedule audio (sec)

      let currentNote = 0; // The note we are currently playing
      let nextNoteTime = 0.0; // when the next note is due.
      function nextNote() {
        const secondsPerBeat = 60.0 / tempo;

        nextNoteTime += secondsPerBeat; // Add beat length to last beat time

        // Advance the beat number, wrap to zero when reaching 4
        currentNote = (currentNote + 1) % 4;
      }

      // Create a queue for the notes that are to be played, with the current time that we want them to play:
      const notesInQueue = [];
      let dtmf;

      function scheduleNote(beatNumber, time) {
        // Push the note into the queue, even if we're not playing.
        notesInQueue.push({ note: beatNumber, time: time });

        if (pads[0].querySelectorAll("input")[beatNumber].checked) {
          playSweep(time);
        }
        if (pads[1].querySelectorAll("input")[beatNumber].checked) {
          playPulse(time);
        }
        if (pads[2].querySelectorAll("input")[beatNumber].checked) {
          playNoise(time);
        }
        if (pads[3].querySelectorAll("input")[beatNumber].checked) {
          playSample(audioCtx, dtmf, time);
        }
      }

      let timerID;
      function scheduler() {
        // While there are notes that will need to play before the next interval,
        // schedule them and advance the pointer.
        while (nextNoteTime < audioCtx.currentTime + scheduleAheadTime) {
          scheduleNote(currentNote, nextNoteTime);
          nextNote();
        }
        timerID = setTimeout(scheduler, lookahead);
      }

      // Draw function to update the UI, so we can see when the beat progress.
      // This is a loop: it reschedules itself to redraw at the end.
      let lastNoteDrawn = 3;
      function draw() {
        let drawNote = lastNoteDrawn;
        const currentTime = audioCtx.currentTime;

        while (notesInQueue.length && notesInQueue[0].time < currentTime) {
          drawNote = notesInQueue[0].note;
          notesInQueue.shift(); // Remove note from queue
        }

        // We only need to draw if the note has moved.
        if (lastNoteDrawn !== drawNote) {
          pads.forEach((pad) => {
            pad.children[lastNoteDrawn * 2].style.borderColor = "var(--black)";
            pad.children[drawNote * 2].style.borderColor = "var(--yellow)";
          });

          lastNoteDrawn = drawNote;
        }
        // Set up to draw again
        requestAnimationFrame(draw);
      }

      // When the sample has loaded, allow play
      const loadingEl = document.querySelector(".loading");
      const playButton = document.querySelector("#playBtn");
      let isPlaying = false;
      setupSample().then((sample) => {
        loadingEl.style.display = "none";

        dtmf = sample; // to be used in our playSample function

        playButton.addEventListener("click", (ev) => {
          isPlaying = !isPlaying;

          if (isPlaying) {
            // Start playing

            // Check if context is in suspended state (autoplay policy)
            if (audioCtx.state === "suspended") {
              audioCtx.resume();
            }

            currentNote = 0;
            nextNoteTime = audioCtx.currentTime;
            scheduler(); // kick off scheduling
            requestAnimationFrame(draw); // start the drawing loop.
            ev.target.dataset.playing = "true";
          } else {
            window.clearTimeout(timerID);
            ev.target.dataset.playing = "false";
          }
        });
      });
    </script>
  </body>
</html>
